#include "FileSystem.h"
#include "Console.h"
#include "StringManager.h"
#include "Constants.h"
#include "PlatformTarget.h"

namespace CEngine
{

namespace FileSystem {

    namespace {
        FilePath basePath;
        FilePath searchPath;
        U64 searchHash;
    }

    #ifdef WINDOWS
    #include "Windows.h"
    #include <Shlwapi.h>

    namespace {
        void
        printSystemError(U32 error);

        void
        copyDirectoryA(CHAR* source, CHAR* destination, bool overwrite);

        void
        isInitialized(void);
        bool initialized = false;
    }

    void
    initialize(void) {
        CHAR temp[260];
        U32 length = GetModuleFileNameA(NULL, (CHAR*)&temp, 260);
        if(!length) {
            printSystemError(GetLastError());
            return;
        }

        for (U32 i = length; temp[i] != '\\'; --i) {
            --length;
        }
        temp[length + 1] = '\0';

        basePath = temp;

        searchPath = FilePath();
        searchHash = 0;

        initialized = true;
        defaultLog.log(Logging::FILESYSTEM, LOG_INFO, "File system initialized.\n");
    }

    bool
    move(const FilePath& currentPath, const FilePath& newPath) {
        isInitialized();

        if(!currentPath.pathExists()) {
            Console::print("%q%s does not exist!\n%r", Console::LWHITE + (16 * Console::LRED), currentPath.getFullPath());
            return false;
        }

        CHAR currentDrive = currentPath.getDriveLetter();
        CHAR newDrive = newPath.getDriveLetter();

        if(!currentDrive) {
            currentDrive = basePath.getDriveLetter();
        }
        if(!newDrive) {
            newDrive = basePath.getDriveLetter();
        }

        if(currentDrive == newDrive) {
            if(!newPath.pathExists()) {
                File temp(newPath, CE_FILE_CREATE_NEW);
                temp.open();
                temp.close();
            }

            if(!ReplaceFileA(newPath.getFullPath(), currentPath.getFullPath(), NULL, 0, NULL, NULL)) {
                printSystemError(GetLastError());
                return false;
            }
        }
        else {
            if(!MoveFileExA(currentPath.getFullPath(), newPath.getFullPath(), MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING)) {
                printSystemError(GetLastError());
                return false;
            }
        }
        return true;
    }

    bool
    move(File& currentFile, const FilePath& newPath) {
        isInitialized();

        FilePath currentPath = currentFile.getPath();
        bool wasOpen = currentFile.isOpen();
        currentFile.close();
        if(move(currentPath, newPath)) {
            currentFile.setPath(newPath);
            if(wasOpen) {
                U32 flags = currentFile.getFlags();
                currentFile.setFlags((flags & (0xFFFFFFFF - 0x1C)) + 0xC);
                currentFile.open();
                currentFile.setFlags(flags);
            }

            return true;
        }
        if(wasOpen) {
            currentFile.open();
        }
        return false;
    }

    bool
    copy(const FilePath& currentPath, const FilePath& newPath, bool overwrite) {
        isInitialized();

        if(!currentPath.pathExists()) {
            Console::print("%q%s does not exist!\n%r", Console::LWHITE + (16 * Console::LRED), currentPath.getFullPath());
            return false;
        }
        bool inputIsDirectory = currentPath.isDirectory();
        bool outputMustBeFile = true;
        if(newPath.getExtension() == '\0') {
            outputMustBeFile = false;
        }

        if(inputIsDirectory && outputMustBeFile) {
            Console::print("%q%s is not a file!\n%r", Console::LWHITE + (16 * Console::LRED), currentPath.getFullPath());
            return false;
        }

        if(inputIsDirectory) {
            copyDirectoryA(currentPath.getFullPath(), newPath.getFullPath(), overwrite);
        }
        else {
            if(!CopyFileA(currentPath.getFullPath(), newPath.getFullPath(), !overwrite)) {
                printSystemError(GetLastError());
                return false;
            }
        }
        return true;
    }

    bool
    copy(File& currentFile, const FilePath& newPath, bool overwrite) {
        isInitialized();

        FilePath currentPath = currentFile.getPath();
        bool wasOpen = currentFile.isOpen();
        currentFile.close();
        if(copy(currentPath, newPath, overwrite)) {
            currentFile.setPath(newPath);
            if(wasOpen) {
                U32 flags = currentFile.getFlags();
                currentFile.setFlags((flags & (0xFFFFFFFF - 0x1C)) + 0xC);
                currentFile.open();
                currentFile.setFlags(flags);
            }

            return true;
        }
        if(wasOpen) {
            currentFile.open();
        }
        return false;
    }

    bool
    remove(File& file, const bool close) {
        isInitialized();

        if(!DeleteFileA(file.getPath().getFullPath())) {
            printSystemError(GetLastError());
            return false;
        }

        if(close) {
            file.close();
        }
        return true;
    }

    bool
    remove(const FilePath& path) {
        isInitialized();

        if(!path.pathExists()) {
            Console::print("%q%s does not exist!\n%r", Console::LWHITE + (16 * Console::LRED), path.getFullPath());
            return false;
        }

        if(!path.isDirectory()) {
            if(!DeleteFileA(path.getFullPath())) {
                printSystemError(GetLastError());
                return false;
            }
        }
        else {
            CHAR tempPath[261];
            U16 i = 0;
            for (; i < path.getLength(); ++i) {
                tempPath[i] = path.getFullPath()[i];
            }
            tempPath[i] = '\0';
            tempPath[i + 1] = '\0';

            SHFILEOPSTRUCTA temp = { 0 };
            temp.wFunc = FO_DELETE;
            temp.pFrom = (const CHAR*)tempPath;
            temp.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR;

            if(SHFileOperationA(&temp) || temp.fAnyOperationsAborted) {
                U32 error = GetLastError();
                if(!temp.fAnyOperationsAborted && ((error < 0x71 || error > 0x88) && error != 0xB7)) {
                    printSystemError(error);
                }
                else {
                    Console::print("Could not delete folder. Non-standard error code: %#X\n", error);
                }
                return false;
            }
        }
        return true;
    }

    void
    relativeToAbsolute(FilePath& path) {
        isInitialized();

        CHAR temp[260];

        for (U32 i = 0; i <= basePath.getLength(); ++i) {
            temp[i] = basePath.getFullPath()[i];
        }

        PathAppendA(temp, path.getFullPath());

        path = temp;
    }

    void
    absoluteToRelative(FilePath& path) {
        isInitialized();

        CHAR temp[260];
        if(path.isDirectory()) {
            if(PathRelativePathToA(temp, basePath.getFullPath(), FILE_ATTRIBUTE_DIRECTORY, path.getFullPath(), FILE_ATTRIBUTE_DIRECTORY)) {
                path = temp;
            }
        }
        else {
            if(PathRelativePathToA(temp, basePath.getFullPath(), FILE_ATTRIBUTE_DIRECTORY, path.getFullPath(), FILE_ATTRIBUTE_NORMAL)) {
                path = temp;
            }
        }
    }

    bool
    listDirectory(FileInfo& entry, FilePath* path) {
        isInitialized();

        static HANDLE handle = NULL;
        bool newSearch = path;
        entry.fileName = FilePath();
        entry.timeLastModified = 0;
        entry.size = 0;
        entry.isDirectory = false;

        WIN32_FIND_DATAA data;

        if(newSearch) {
            U64 hash = StringManager::hashString(path->getFullPath());
            hash = (hash * Constants::String::hashSeed) + '*';

            newSearch = !(hash == searchHash);
        }

        if(newSearch) {
            if(handle) {
                if(!FindClose(handle)) {
                    printSystemError(GetLastError());
                    return false;
                }
                handle = NULL;
            }

            CHAR temp[261];
            for (U32 i = 0; i <= path->getLength(); ++i) {
                temp[i] = path->getFullPath()[i];
                if(temp[i] == '\0') {
                    temp[i] = '*';
                    temp[i + 1] = '\0';
                }
            }
            searchHash = StringManager::hashString(temp);
            searchPath = temp;

            handle = FindFirstFileA(searchPath.getFullPath(), &data);

            if(handle == INVALID_HANDLE_VALUE) {
                printSystemError(GetLastError());
                return false;
            }

            entry.fileName = data.cFileName;

            if((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) {
                entry.isDirectory = true;
            }

            ULARGE_INTEGER large;
            large.LowPart = data.ftLastWriteTime.dwLowDateTime;
            large.HighPart = data.ftLastWriteTime.dwHighDateTime;
            entry.timeLastModified = large.QuadPart;

            large.LowPart = data.nFileSizeLow;
            large.HighPart = data.nFileSizeHigh;
            entry.size = large.QuadPart;

            return true;
        }

        if(!handle) {
            return false;
        }

        if(!FindNextFileA(handle, &data)) {
            U32 error = GetLastError();
            if (error != ERROR_NO_MORE_FILES) {
                printSystemError(error);
                return false;
            }
            FindClose(handle);
            handle = NULL;
            return false;
        }

        entry.fileName = data.cFileName;

        if((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) {
            entry.isDirectory = true;
        }

        ULARGE_INTEGER large;
        large.LowPart = data.ftLastWriteTime.dwLowDateTime;
        large.HighPart = data.ftLastWriteTime.dwHighDateTime;
        entry.timeLastModified = large.QuadPart;

        large.LowPart = data.nFileSizeLow;
        large.HighPart = data.nFileSizeHigh;
        entry.size = large.QuadPart;

        return true;
    }

    namespace {
        void
        printSystemError(U32 error) {
            U32 bufferSize = 1024;
            CHAR buffer[bufferSize];

            FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,
                          NULL,
                          error,
                          0,
                          buffer,
                          bufferSize - 1,
                          NULL);
            Console::print("%q%s%r", Console::LWHITE + (16 * Console::LRED), buffer);
        }

        void
        copyDirectoryA(CHAR* cszFrom, CHAR* cszTo, bool overwrite) {
            CreateDirectoryA(cszTo, NULL);

            WIN32_FIND_DATA FindFileData;
            HANDLE hFindFile;

            CHAR cszDirectoryFindPattern[MAX_PATH] = {0};
            Console::sPrint(cszDirectoryFindPattern, MAX_PATH, "%s\\*", cszFrom);

            if ((hFindFile = FindFirstFileA(cszDirectoryFindPattern, &FindFileData)) != INVALID_HANDLE_VALUE) {
                    do {
                            if (*FindFileData.cFileName == '.')
                                    continue;

                            CHAR cszFileOrDirectoryFrom[MAX_PATH] = {0};
                            CHAR cszFileOrDirectoryTo[MAX_PATH] = {0};
                            Console::sPrint(cszFileOrDirectoryFrom, MAX_PATH, "%s\\%s", cszFrom, FindFileData.cFileName);
                            Console::sPrint(cszFileOrDirectoryTo, MAX_PATH, "%s\\%s", cszTo, FindFileData.cFileName);

                            if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                                    copyDirectoryA(cszFileOrDirectoryFrom, cszFileOrDirectoryTo, overwrite);
                            }
                            else {
                                    CopyFileA(cszFileOrDirectoryFrom, cszFileOrDirectoryTo, !overwrite);
                            }
                    } while(FindNextFile(hFindFile, &FindFileData));
                    FindClose(hFindFile);
            }
        }
    }

    #endif // WINDOWS

    CHAR*
    getBasePath(void) {
        isInitialized();
        return basePath.getFullPath();
    }

    namespace {
        void
        isInitialized(void) {
            if(!initialized) {
                defaultLog.log(Logging::FILESYSTEM, LOG_ERROR, "Attempted to use file system before it was initialized!\n");
            }
        }
    }
}

}
